<?php

/**
 * @copyright Michiel Hakvoort 2010
 * @license http://www.opensource.org/licenses/bsd-license.php New BSD
 * @package mangrove
 * @subpackage groove
 * @filesource
 */

/*
 * Copyright (c) 2010 Michiel Hakvoort
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

namespace mg;

use \ArrayIterator;

class TempletParser {

    public function parse($file) {
        $inputStream = new StringInputStream(file_get_contents($file, FILE_TEXT));

        $templetParser = new TempletFileParser();
        $lexer = new RootLexer($inputStream, $templetParser);
        $lexer->tokenize();

        return $templetParser->getTemplets();
    }
}

class TempletFileParser implements GrooveParser {

    private $depth = 0;

    private $previousToken = null;

    private $capture = null;

    private $rule = array();
    private $rules = array();

    private $templets = array();

    private $tokens = null;

    private $namespace = null;
    private $uses = array();

    private $use = null;

    private $inContextImport = false;

    private $contextImports = array();

    public function __construct() {
    }

    public function parse(GrooveToken $token) {
        $ident = $token->getIdent();
        $previousTokenIdent = $this->previousToken === null ? null : $this->previousToken->getIdent();

        if($this->depth === 0) {
            switch($ident) {
                case GrooveToken :: T_COMMENT :
                case GrooveToken :: T_DOC_COMMENT :
                    return;
                case GrooveToken :: T_CONTEXT :
                    $this->inContextImport = true;
                    break;
                case GrooveToken :: T_USE :
                case GrooveToken :: T_AS :
                    break;
                case GrooveToken :: T_SEMICOLON :
                    if($this->inContextImport) {
                        $this->inContextImport = false;
                        break;
                    }
                    if($this->use !== null) {
                        $use = $this->use;
                        if(($nsdelimPos = mb_strrpos($use, '\\')) !== false) {
                            $use = mb_substr($use, $nsdelimPos + 1);
                        }
                        $this->uses[$use] = $this->use;
                        $this->use = null;
                    }
                    break;
                case GrooveToken :: T_COMMA :
                    if($this->use !== null) {
                        $use = $this->use;
                        if(($nsdelimPos = mb_strrpos($use, '\\')) !== false) {
                            $use = mb_substr($use, $nsdelimPos + 1);
                        }
                        $this->uses[$use] = $this->use;
                        $this->use = null;
                    }
                    break;
                case GrooveToken :: T_CURLY_OPEN :
                    if(count($this->rule) > 0) {
                        $this->rules[]= $this->rule;
                        $this->rule = array();
                    }
                    $this->depth++;
                    break;
                case GrooveToken :: T_STRING :
                    if($previousTokenIdent === GrooveToken :: T_ASSIGNMENT) {
                        if($this->inContextImport) {
                            $this->contextImports[$this->capture] = $token->getValue();
                        } else {
                            $this->rule[] = array($token->getValue(), $this->capture);
                        }
                        $capture = null;
                    } elseif($previousTokenIdent === GrooveToken :: T_NAMESPACE) {
                        $this->namespace = $token->getValue();
                    } elseif($previousTokenIdent === GrooveToken :: T_USE) {
                        $this->use = mb_strtolower(trim($token->getValue(), '\\'));
                    } elseif($previousTokenIdent === GrooveToken :: T_AS) {
                        $this->uses[mb_strtolower($token->getValue())] = $this->use;
                        $this->use = null;
                    } elseif($previousTokenIdent === GrooveToken :: T_COMMA) {
                        $this->use = mb_strtolower(trim($token->getValue(), '\\'));
                    } else {
                        $this->rule[] = array($token->getValue(), null);
                    }
                    break;
                case GrooveToken :: T_VARIABLE :
                    $this->capture = $token->getValue();
                    break;
                case GrooveToken :: T_EQUALS :
                    break;
                case GrooveToken :: T_MATCH_SEPARATOR :
                    // Add another match
                    if(count($this->rule) > 0) {
                        $this->rules[]= $this->rule;
                        $this->rule = array();
                    }
                    break;
                default :
                    break;
            }
        } else {
            switch($ident) {
                case GrooveToken :: T_CURLY_CLOSE :
                    $this->depth--;
                    break;
                case GrooveToken :: T_CURLY_OPEN :
                    $this->depth++;
                    break;
            }

            if($this->depth !== 0) {
                $this->tokens[] = $token;
            } else {
                $parser = new CommandParser($this->tokens);

                $command = $parser->getAST();

                $matches = array();
                $ruleAST = null;

                foreach($this->rules as $rule) {
                    $matchArr = $rule[count($rule)-1];
                    $match = null;

                    for($i = count($rule)-1; $i >= 0; $i--) {
                        $match = new MatchAST($rule[$i][1], $rule[$i][0], $match);
                    }

                    $ruleAST = new RuleAST($match, $ruleAST);
                }

                $templet = new TempletAST($command, $ruleAST, $this->namespace, $this->uses, $this->contextImports);
                $this->templets[] = $templet;
                $this->rule = array();
                $this->rules = array();
                $this->tokens = null;
            }
        }


        $this->previousToken = $token;
    }

    public function getTemplets() {
        return $this->templets;
    }
}

class CommandParser {

    /**
     * Enter description here...
     *
     * @var ArrayIterator
     */
    private $tokens = null;

    /**
     * Enter description here...
     *
     * @var Token
     */
    private $currentToken = null;

    private $ast = null;

    public function __construct($tokens) {
        if($tokens === null) {
            $tokens = array();
        }
        $this->tokens =  new ArrayIterator($tokens);
        $this->tokens->rewind();
        $this->currentToken = $this->tokens->current();

        $this->parse();
    }

    public function getAST() {
        return $this->ast;
    }

    /**
     * Enter description here...
     *
     * @param int $expectIdent
     * @return mgToken
     */
    protected function accept($expectIdent = null) {
        if($expectIdent != null) {
            if($this->currentToken === null) {
                throw new ParseException('Unexpected end of tokens');
            } else if($this->currentToken->getIdent() !== $expectIdent) {
                throw new ParseException('Expected \'' . GrooveToken :: getName($expectIdent) . '\' got \''.GrooveToken :: getName($this->currentToken->getIdent()).'\'');
            }
        }

        $this->tokens->next();
        $result = $this->currentToken;
        $this->currentToken = $this->tokens->current();
        return $result;
    }

    protected function parse() {
        $this->ast = $this->parseCommand();
    }

    protected function parseCommand() {
        $command = $this->parseSingleCommand();

        $continue = true;

        while($continue && $this->currentToken != null) {
            $nextCommand = $this->parseSingleCommand();
            if($nextCommand == null) {
                $continue = false;
                continue;
            }
            $command = new SequentialCommandAST($command, $nextCommand);
        }

        return $command;
    }

    protected function parseSingleCommand() {
        $command = null;

        if($this->currentToken === null) {
            return null;
        }

        switch($this->currentToken->getIdent()) {
            case GrooveToken :: T_PARENTHESIS_OPEN :
                $command = $this->parseTranslate();
                break;
            case GrooveToken :: T_VARIABLE :
                $command = $this->parseExpression();
                break;
            case GrooveToken :: T_INLINE_HTML :
                $command = new TextCommandAST($this->currentToken->getValue());
                $this->accept(GrooveToken :: T_INLINE_HTML);
                break;
            case GrooveToken :: T_SCOPE :
                $command = $this->parseScope();
                break;
            case GrooveToken :: T_CACHE :
                $command = $this->parseCache();
                break;
            case GrooveToken :: T_IF :
                $command = $this->parseIf();
                break;
            case GrooveToken :: T_OPEN_TAG :
                $command = $this->parsePhpOpen();
                break;
            case GrooveToken :: T_OPEN_TAG_WITH_ECHO :
                $command = $this->parsePhpOpenWithEcho();
                break;
            case GrooveToken :: T_FOREACH :
                $command = $this->parseForeach();
                break;
        }
        return $command;
    }

    protected function parseTranslate() {
        $this->accept(GrooveToken :: T_PARENTHESIS_OPEN);

        $lookup = null;

        switch($this->currentToken->getIdent()) {
            case GrooveToken :: T_PARENTHESIS_OPEN :
                $lookup = $this->parseTranslate();
                break;
            case GrooveToken :: T_VARIABLE :
                $lookup = $this->parseExpression();
                break;
            case GrooveToken :: T_STRING :

                $lookup = new SymbolAST($this->currentToken->getValue());
                $this->accept(Token :: T_STRING);
                break;
        }

        $parameters = array();

        while($this->currentToken->getIdent() == GrooveToken :: T_COMMA) {
            $this->accept(GrooveToken :: T_COMMA);

            switch($this->currentToken->getIdent()) {
                case GrooveToken :: T_PARENTHESIS_OPEN:
                    $parameters[] = $this->parseTranslate();
                    break;
                case GrooveToken :: T_VARIABLE :
                    $parameters[] = $this->parseExpression();
                    break;
                case GrooveToken :: T_STRING :
                    $parameters[] = new SymbolAST($this->currentToken->getValue());
                    $this->accept(GrooveToken :: T_STRING);
                    break;
            }
        }

        $this->accept(GrooveToken :: T_PARENTHESIS_CLOSE);

        $expression = new TranslateAST($lookup, $parameters);

        if($this->currentToken !== null && $this->currentToken->getIdent() === GrooveToken :: T_PIPE) {
            $this->accept(GrooveToken :: T_PIPE);
            $symbol = $this->accept(GrooveToken :: T_STRING);
            $parameters = $this->parseParameters();
            $expression = new FilterExpressionAST($expression, $symbol->getValue(), $parameters);
        }

        while($this->currentToken !== null && in_array($this->currentToken->getIdent(), array(GrooveToken :: T_OBJECT_OPERATOR, GrooveToken :: T_PIPE))) {
            if($this->currentToken->getIdent() === GrooveToken :: T_PIPE) {
                $this->accept(GrooveToken :: T_PIPE);
                $symbol = $this->accept(GrooveToken :: T_STRING);
                $parameters = $this->parseParameters();
                $expression = new FilterExpressionAST($expression, $symbol->getValue(), $parameters);
            } else {
                $this->accept(GrooveToken :: T_OBJECT_OPERATOR);
                $symbol = $this->accept(GrooveToken :: T_STRING);
                $parameters = $this->parseParameters();
                $expression = new MethodExpressionAST($expression, $symbol->getValue(), $parameters);
            }
        }

        return $expression;
    }

    protected function parseExpression() {
        $token = $this->accept(GrooveToken :: T_VARIABLE);

        $expression = new VariableExpressionAST($token->getValue());

        while($this->currentToken !== null && in_array($this->currentToken->getIdent(), array(GrooveToken :: T_OBJECT_OPERATOR, GrooveToken :: T_PIPE))) {
            if($this->currentToken->getIdent() == GrooveToken :: T_PIPE) {
                $this->accept(GrooveToken :: T_PIPE);
                $symbol = $this->accept(GrooveToken :: T_STRING);
                $parameters = $this->parseParameters();
                $expression = new FilterExpressionAST($expression, $symbol->getValue(), $parameters);
            } else {
                $this->accept(GrooveToken :: T_OBJECT_OPERATOR);
                $symbol = $this->accept(GrooveToken :: T_STRING);
                $parameters = $this->parseParameters();
                $expression = new MethodExpressionAST($expression, $symbol->getValue(), $parameters);
            }
        }

        return $expression;

    }

    protected function parseParameters() {
        if($this->currentToken === null || $this->currentToken->getIdent() !== GrooveToken :: T_PARENTHESIS_OPEN) {
            return null;
        }

        $this->accept();

        $result = array();


        $parameter = array();

        $depth = 0;

        while($this->currentToken !== null && $this->currentToken->getIdent() !== GrooveToken :: T_PARENTHESIS_CLOSE) {
            if($this->currentToken->getIdent() === GrooveToken :: T_CHARACTER && $this->currentToken->getValue() === '(') {
                $depth++;
                $parameter []= $this->currentToken;
            } elseif($this->currentToken->getIdent() === GrooveToken :: T_CHARACTER && $this->currentToken->getValue() === ')') {
                $depth--;
                $parameter []= $this->currentToken;
            }elseif($depth === 0 && $this->currentToken->getIdent() === GrooveToken :: T_CHARACTER && $this->currentToken->getValue() === ',') {
                $result[] = $parameter;
                $parameter = array();
            } else {
                $parameter []= $this->currentToken;
            }

            $this->accept();
        }

        $result[] = $parameter;
        $this->accept(GrooveToken :: T_PARENTHESIS_CLOSE);
        return $result;
    }

    protected function parseScope() {
        $this->accept(GrooveToken :: T_SCOPE);
        $parameters = $this->parseParameters();
        $this->accept(GrooveToken :: T_CURLY_OPEN);
        $command = $this->parseCommand();
        $this->accept(GrooveToken :: T_CURLY_CLOSE);

        $scope = null;

        if(count($parameters) > 0) {
            $scope = $parameters;
        }

        return new ScopeAST($command, $scope);
    }

    protected function parseCache() {
        $this->accept(GrooveToken :: T_CACHE);
        $parameters = $this->parseParameters();
        $this->accept(GrooveToken :: T_CURLY_OPEN);
        $command = $this->parseCommand();
        $this->accept(GrooveToken :: T_CURLY_CLOSE);

        $duration = count($parameters) > 0 ? $parameters[0] : null;
        $key = count($parameters) > 1 ? $parameters[1] : null;

        return new CacheAST($command, $duration, $key);
    }

    protected function parseForeach() {
        $this->accept(GrooveToken :: T_FOREACH);

        $parameters = $this->parseParameters();

        $condition = count($parameters) ? $parameters[0] : null;

        $this->accept(GrooveToken :: T_CURLY_OPEN);

        $command = $this->parseCommand();

        $this->accept(GrooveToken :: T_CURLY_CLOSE);

        if($this->currentToken === null) {
            return new ForeachAST($condition, $command, null);
        }

        $else = null;
        switch($this->currentToken->getIdent()) {
            case GrooveToken :: T_ELSE :
                $this->accept(GrooveToken :: T_ELSE);
                $this->accept(GrooveToken :: T_CURLY_OPEN);
                $else = $this->parseCommand();
                $this->accept(GrooveToken :: T_CURLY_CLOSE);
                break;
        }

        return new ForeachAST($condition, $command, $else);
    }

    protected function parseIf() {
        $this->accept();

        $parameters = $this->parseParameters();

        $condition = count($parameters) ? $parameters[0] : null;

        $this->accept(GrooveToken :: T_CURLY_OPEN);

        $command = $this->parseCommand();

        $this->accept(GrooveToken :: T_CURLY_CLOSE);

        if($this->currentToken === null) {
            return new IfAST($condition, $command, null);
        }

        $else = null;
        switch($this->currentToken->getIdent()) {
            case GrooveToken :: T_ELSE :
                $this->accept(GrooveToken :: T_ELSE);
                $this->accept(GrooveToken :: T_CURLY_OPEN);
                $else = $this->parseCommand();
                $this->accept(GrooveToken :: T_CURLY_CLOSE);
                break;
            case GrooveToken :: T_ELSEIF :
                $else = $this->parseIf();
                break;
        }

        return new IfAST($condition, $command, $else);
    }

    protected function parsePhpOpen() {
        $this->accept(GrooveToken :: T_OPEN_TAG);
        $tokens = array();

        while($this->currentToken !== null && $this->currentToken->getIdent() !== GrooveToken :: T_CLOSE_TAG) {
            $tokens[] = $this->currentToken;
            $this->accept();
        }

        // TODO : fix this
        $this->accept(GrooveToken :: T_CLOSE_TAG);
        $this->accept(GrooveToken :: T_CLOSE_TAG);

        return new PhpAST($tokens);
    }

    protected function parsePhpOpenWithEcho() {
        $this->accept(GrooveToken :: T_OPEN_TAG_WITH_ECHO);
        $tokens = array();

        while($this->currentToken !== null && $this->currentToken->getIdent() !== GrooveToken :: T_CLOSE_TAG) {
            $tokens[] = $this->currentToken;
            $this->accept();
        }

        $this->accept(GrooveToken :: T_CLOSE_TAG);
        $this->accept(GrooveToken :: T_CLOSE_TAG);

        return new PhpEchoAST($tokens);
    }

}

